iT邦幫忙

2022 iThome 鐵人賽

DAY 11
0

今日目標,加入各種限制來完善註冊功能。

Validation

我們定義註冊必須滿足一些條件:Email、Username 必須唯一,而且 Password 長度不能少於 8

  1. 加入依賴:
    <!-- Validation -->
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 添加 constraint 的註解,將 UserModel 修改為以下內容:
    package com.example.user;
    
    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    import javax.validation.constraints.Email;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.Size;
    
    @Entity
    @Table(name = "user")
    @Getter @Setter
    public class UserModel {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
    
        @Column(unique = true)
        @NotBlank(message = "帳號不可為空")
        private String username;
    
        @Column(unique = true)
        @Email(message = "信箱格式錯誤")
        @NotBlank(message = "信箱不可為空")
        private String email;
    
        @Column
        @Size(min = 8, message = "密碼不可少於8位")
        @NotBlank(message = "密碼不可為空")
        private String password;
    }
    
    • @Email:聲明該欄位必須為 Email 的格式
    • @NotBlank:聲明該欄位不能空白
    • @Size:聲明該欄位的大小(min, max)
    • 這些 constraint 聲明都擁有 message 可以回傳錯誤訊息
  3. 修改 UserService,檔案內容完整為:
    package com.example.user;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.validation.annotation.Validated;
    
    import javax.validation.ConstraintViolation;
    import javax.validation.ConstraintViolationException;
    import javax.validation.Validator;
    import java.util.Set;
    
    @Service
    @Validated
    public class UserService {
        @Autowired
        private UserRepository userRepository;
    
        @Autowired
        private Validator validator;
    
        public Integer addUser(UserModel user) {
            Set<ConstraintViolation<UserModel>> violations = validator.validate(user);
            if (!violations.isEmpty()) {
                StringBuilder sb = new StringBuilder();
                for (ConstraintViolation<UserModel> constraintViolation : violations) {
                    sb.append(constraintViolation.getMessage());
                }
                throw new ConstraintViolationException(sb.toString(), violations);
            }
            UserModel newUser = userRepository.save(user);
            return newUser.getId();
        }
    }
    
    • 如同前面介紹提到的,Service Layer 負責檢驗資料,所以在這邊檢驗是否符合我們的註冊規則
    • 主要透過 validator.validate 檢驗資料,當有欄位不通過檢驗,就會將其蒐集,並透過 ConstraintViolationException 來將錯誤訊息以及錯誤本身丟出,而丟出的 ConstraintViolationException 將會在 controller 捕獲並處理
  4. 修改 UserController 的 registerProcess,檔案完整內容為:
    package com.example.user;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.servlet.mvc.support.RedirectAttributes;
    
    import javax.validation.Valid;
    import java.util.Objects;
    
    @Controller
    public class UserController {
        @Autowired
        private UserService userService;
    
        @GetMapping("/register")
        public String viewRegisterPage(Model model) {
            model.addAttribute("name", "註冊");
            model.addAttribute("user", new UserModel());
            return "register";
        }
    
        @PostMapping("/register")
        public String registerProcess(@Valid UserModel user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
            if (bindingResult.hasErrors()) {
                String message = Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage();
                redirectAttributes.addFlashAttribute("error", message);
                return "redirect:/register";
            }
            userService.addUser(user);
            return "redirect:/";
        }
    }
    
    • registerProcess 的參數 BindingResult bindingResult 將會負責捕獲錯誤,藉由 bindingResult.hasErrors() 來確認是否有錯誤被捕獲,如果有就使用 bindingResult.getFieldError().getDefaultMessage() 來取得錯誤訊息
    • registerProcess 的參數 RedirectAttributes redirectAttributes 用來傳遞一次性的重導向參數,當使用者輸入的資訊未通過檢驗,將其重新導向註冊頁面,並給予訊息提示,而訊息就來自 redirectAttributes.addFlashAttribute("error", message) 的設定
  5. 修改一下 register.html 使其可以接受並顯示錯誤訊息,檔案完整內容為:
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1 th:text="${name}"></h1>
        <div th:if="${error}">
            <div th:text="${error}"></div>
        </div>
        <form method="post" action="/register" th:object="${user}">
            <input type="email" id="email" name="email" placeholder="Email" th:field="*{email}" />
            <input type="text" id="username" name="username" placeholder="Username" th:field="*{username}" />
            <input type="password" id="password" name="password" placeholder="Password" th:field="*{password}" />
            <button type="submit">註冊</button>
        </form>
    </body>
    </html>
    
    • th:if="${error}":當 error 變數存在時,就顯示出錯誤訊息
  6. Demo Time,測試看看吧,到這邊我們解決了很多繁瑣的驗證過程,但還有一個東西我們還沒處理,那就是 Email、Username 的唯一性
    /images/emoticon/emoticon28.gif

自定義 Validation Constraint

我們如果要擋掉重複的 Email 或 Username 註冊,一種做法是自己定義一個新的驗證約束註解(validation constraint annoation),另一種是捕獲資料庫的 ConstraintViolationException,小弟我試了很多次,發現實在捕獲不到資料庫的錯誤,所以就自己定義規則囉,而且這樣也比較容易,不必再新增 Service 驗證,只需要原本的 Validator 即可

  1. 在 UserService 新增查詢方法 findUserByEmailfindUserByUsername,完整檔案內容為:
    package com.example.user;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.validation.annotation.Validated;
    
    import javax.validation.ConstraintViolation;
    import javax.validation.ConstraintViolationException;
    import javax.validation.Validator;
    import java.util.Set;
    
    @Service
    @Validated
    public class UserService {
        @Autowired
        private UserRepository userRepository;
    
        @Autowired
        private Validator validator;
    
        public UserModel findUserByEmail(String email) {
            return userRepository.findByEmail(email);
        }
    
        public UserModel findUserByUsername(String username) {
            return userRepository.findByUsername(username);
        }
    
        public Integer addUser(UserModel user) {
            Set<ConstraintViolation<UserModel>> violations = validator.validate(user);
            if (!violations.isEmpty()) {
                StringBuilder sb = new StringBuilder();
                for (ConstraintViolation<UserModel> constraintViolation : violations) {
                    sb.append(constraintViolation.getMessage());
                }
                throw new ConstraintViolationException(sb.toString(), violations);
            }
            UserModel newUser = userRepository.save(user);
            return newUser.getId();
        }
    }
    
  2. 定義註解,在 user package 底下建立一個 java annoation,名稱為 UniqueEmail,變更檔案內容為:
    package com.example.user;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Constraint(validatedBy = UniqueEmailValidator.class)
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface UniqueEmail {
        String message() default "此信箱已被使用,請換一組";
        Class<?>[] groups() default { };
        Class<? extends Payload>[] payload() default { };
    }
    
    • @Constraint:透過 validatedBy 來告知使用什麼驗證器(validator)來作驗證
      • 這邊表示我們使用 UniqueEmailValidator 做驗證器,再往下才會建立,所以這邊是紅字的,不必擔心~
    • @Target:決定註解聲明的對象和範圍,由 ElementType 來定義
      • ElementType.FIELD:允許作用在屬性上
      • ElementType.METHOD:允許作用在方法上
      • ElementType.PARAMETER:允許作用在參數上
    • @Retention:決定這個註解聲明存在的生命週期,由 RetentionPolicy 來定義
      • RetentionPolicy.RUNTIME:在運行時有效
      • RetentionPolicy.CLASS:在 class 檔案中有效
      • RetentionPolicy.SOURCE:在 source code 中有效
  3. 定義註解的驗證器,在 user package 底下建立一個 java class,名稱為 UniqueEmailValidator,變更檔案內容為:
    package com.example.user;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    @Component
    public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
        @Autowired
        private UserService userService;
    
        @Override
        public boolean isValid(String email, ConstraintValidatorContext context) {
            return (userService.findUserByEmail(email) == null);
        }
    }
    
    • 藉由實現(implements) ConstraintValidator 來實作驗證器,而後方的參數第一個是放註解類,第二個則是要檢驗的資料類型,所以這邊表示我們用 UniqueEmail 註釋來檢驗 輸入的 email(String)
    • 透過覆寫 isValid 來實現自定義檢驗規則,這邊是當我無法用 email 找到 user(就是 null)表示合法,因為我們不希望有 email 重複
  4. 定義註解,在 user package 底下建立一個 java annoation,名稱為 UniqueUsername,變更檔案內容為:
    package com.example.user;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Constraint(validatedBy = UniqueUsernameValidator.class)
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface UniqueUsername {
        String message() default "此帳號已被使用,請換一組";
        Class<?>[] groups() default { };
        Class<? extends Payload>[] payload() default { };
    }
    
  5. 定義註解的驗證器,在 user package 底下建立一個 java class,名稱為 UniqueUsernameValidator,變更檔案內容為:
    package com.example.user;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    @Component
    public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {
        @Autowired
        private UserService userService;
    
        @Override
        public boolean isValid(String username, ConstraintValidatorContext context) {
            return (userService.findUserByUsername(username) == null);
        }
    }
    
  6. 在 UserModel 添加剛才自定義的 constraint,完整檔案內容為:
    package com.example.user;
    
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    import javax.persistence.*;
    import javax.validation.constraints.Email;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.Size;
    
    @Entity
    @Table(name = "user")
    @Getter @Setter
    public class UserModel {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
    
        @Column(unique = true)
        @NotBlank(message = "帳號不可為空")
        @UniqueUsername
        private String username;
    
        @Column(unique = true)
        @Email(message = "信箱格式錯誤")
        @NotBlank(message = "信箱不可為空")
        @UniqueEmail
        private String email;
    
        @Column
        @Size(min = 8, message = "密碼不可少於8位")
        @NotBlank(message = "密碼不可為空")
        private String password;
    }
    
  7. It's demo time~~ 這次就來去試試看是不是真的有擋掉重複的 email 和 username 吧!
  8. 不過我們還有一個問題沒有解決,密碼以明文存在資料庫,這是比較不安全的,小弟將在明天介紹 web security 的配置,並加入雜湊密碼的過程~~ /images/emoticon/emoticon13.gif

參考資料


上一篇
Day 09 - 註冊功能
下一篇
Day 11 - Web Security Config
系列文
Spring Boot... 深不可測31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言